iT邦幫忙

2025 iThome 鐵人賽

DAY 23
1

過去幾天,快速開發了許多核心小功能,尤其是語音轉文字筆記。當功能變得越來越複雜,程式碼庫不斷增長時,一個至關重要的問題便浮現出來:如何確保我們今天新增的功能,不會無意間弄壞昨天寫好的程式?

答案就是做「自動化測試」。今天會暫停開發新功能,轉而為我們的App建立第一道防線 —— 單元測試(Unit Test)。我們的目標是為先前建立的Repository和Provider,特別是處理核心邏輯的NoteProvider和FavoriteProvider撰寫測試案例。

一、為何需要單元測試?它與Mock的角色

單元測試是針對程式中最小可測試單元(如一個函式或一個類別)進行的測試。它的核心思想是「隔離」,在不受外界干擾的情況下,驗證這個小單元是否如預期般運作。

單元測試帶來的好處:

  • 確保程式碼品質:在開發階段就能即時發現Bug,避免問題流入到使用者手上。
  • 提供重構的信心:當你想優化或修改程式碼時,只要所有單元測試都能通過,就代表你的修改沒有破壞原有的功能。
  • 作為即時文件:好的測試案例本身就是一份絕佳的文件,清楚地說明了某個函式應該如何被使用。

而在測試Provider時,我們會遇到一個問題:NoteProvider的功能依賴於NoteRepository,而 NoteRepository又依賴於Firestore。在單元測試中,我們只想測試NoteProvider本身的邏輯,並不想真的去讀寫Firestore資料庫。

這就是模擬(Mock)技術的重要性。我們將使用mocktail這個套件來建立一個假的Repository,這個假物件會完全模仿真實Repository的行為,讓我們可以精準控制它的回傳值,從而專心測試Provider的邏輯是否正確。

二、實戰:為FavoriteProvider撰寫測試

我們先從相對簡單的FavoriteProvider開始,假設它有一個toggleFavorite方法,用於新增或移除收藏。

測試步驟 (Arrange-Act-Assert):

Step1. Arrange (安排):

  • 建立一個MockFavoriteRepository。
  • 設定當toggleFavorite方法被呼叫時,我們預期它會做什麼。例如,我們假設它會成功執行並回傳 Future。
  • 建立一個FavoriteProvider的實例,並將剛剛建立的MockFavoriteRepository注入進去。

Step2. Act (執行):
呼叫favoriteProvider.toggleFavorite('some_insight_id')。

Step3. Assert (斷言):

  • 使用verify來驗證 mockRepository.toggleFavorite 是不是真的被以 'some_insight_id' 這個參數呼叫了,並且剛好只被呼叫了一次。
  • 檢查FavoriteProvider內部的狀態(例如 isFavorite 的布林值)是否如預期般被改變了。

測試程式碼示意:

import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

// 假設這是你的 Repository 和 Provider
class FavoriteRepository {
  Future<void> toggleFavorite(String insightId) async { /* ... */ }
}

class FavoriteProvider {
  final FavoriteRepository _repo;
  FavoriteProvider(this._repo);
  
  Future<void> toggle(String id) async {
    await _repo.toggleFavorite(id);
  }
}

// 建立 Mock Class
class MockFavoriteRepository extends Mock implements FavoriteRepository {}

void main() {
  group('FavoriteProvider Tests', () {
    late FavoriteRepository mockRepository;
    late FavoriteProvider favoriteProvider;

    setUp(() {
      mockRepository = MockFavoriteRepository();
      favoriteProvider = FavoriteProvider(mockRepository);
    });

    test('toggle should call repository correctly', () async {
      // Arrange
      when(() => mockRepository.toggleFavorite(any())).thenAnswer((_) async {});

      // Act
      await favoriteProvider.toggle('test_id');

      // Assert
      verify(() => mockRepository.toggleFavorite('test_id')).called(1);
    });
  });
}

三、挑戰:測試NoteProvider的新增筆記功能

NoteProvider撰寫測試,要先測試addNote方法,確保它能正確地呼叫Repository並處理相關狀態。

測試情境:

  • Arrange:建立 MockNoteRepository。設定當addNote被呼叫時,它應該成功返回。同時初始化 NoteProvider,並注入Mock物件。
  • Act:執行 noteProvider.addNote('insight_123', 'This is a test note.')。
  • Assert:驗證mockRepository.addNote 方法被正確的參數呼叫。如果NoteProvider內部有isLoading或isSuccess等狀態,也要一併驗證這些狀態在執行前後是否被正確更新。

透過這種方式,可以確保Provider內的業務邏輯是健全的,無論底層的資料庫發生什麼變化。

明日預告:整合測試

單元測試只解決了「零件」的問題。這些獨立測試過的零件組合在一起後,能否順暢地協同工作呢?明天將探討整合測試 (Integration Test),將模擬從App啟動到使用者完成一個完整操作的端到端流程。


【哈囉你好:)感謝你的閱讀!其他我會常出沒的地方:Threads


上一篇
【30 天做一個極簡App】核心功能:實作語音轉文字筆記
系列文
Mobile Dev|日更靈感來源 App:Flutter × LLM × n8n,每天只推 3 則!23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言